System Modeling

In this example we will see how to model an end-to-end optical system using prysm. Our system will have both an objective lens or telescope as well as a sensor with an optical low-pass filter. We begin by importing the relevant classes and setting some visual styles:

[1]:
from prysm import FringeZernike, PSF, MTF, PixelAperture, OLPF
from matplotlib import pyplot as plt
%matplotlib inline
plt.style.use('bmh')

Next we model the PSF of the objective, given its aperture, focal length, and Zernike coefficients for its wavefront, such as from a Shack-Hartmann sensor or interferometer:

[2]:
# data from a wavefront sensor, optical design program, etc...
coefficients = [0, 0, 0, 0, 0.1, 0.1, -0.025, -0.025, 0.1]

# a circular aperture inscribed in a square 10mm on a side with 50mm EFL
# note the default mask is a circle, so the kwarg is somewhat redundant here.
pupil = FringeZernike(coefficients, dia=10, transmission='circle', z_unit='um', norm=True)
psf = PSF.from_pupil(pupil, efl=40)  # F/2
[3]:
pupil.y
[3]:
array([-5.        , -4.92125984, -4.84251969, -4.76377953, -4.68503937,
       -4.60629921, -4.52755906, -4.4488189 , -4.37007874, -4.29133858,
       -4.21259843, -4.13385827, -4.05511811, -3.97637795, -3.8976378 ,
       -3.81889764, -3.74015748, -3.66141732, -3.58267717, -3.50393701,
       -3.42519685, -3.34645669, -3.26771654, -3.18897638, -3.11023622,
       -3.03149606, -2.95275591, -2.87401575, -2.79527559, -2.71653543,
       -2.63779528, -2.55905512, -2.48031496, -2.4015748 , -2.32283465,
       -2.24409449, -2.16535433, -2.08661417, -2.00787402, -1.92913386,
       -1.8503937 , -1.77165354, -1.69291339, -1.61417323, -1.53543307,
       -1.45669291, -1.37795276, -1.2992126 , -1.22047244, -1.14173228,
       -1.06299213, -0.98425197, -0.90551181, -0.82677165, -0.7480315 ,
       -0.66929134, -0.59055118, -0.51181102, -0.43307087, -0.35433071,
       -0.27559055, -0.19685039, -0.11811024, -0.03937008,  0.03937008,
        0.11811024,  0.19685039,  0.27559055,  0.35433071,  0.43307087,
        0.51181102,  0.59055118,  0.66929134,  0.7480315 ,  0.82677165,
        0.90551181,  0.98425197,  1.06299213,  1.14173228,  1.22047244,
        1.2992126 ,  1.37795276,  1.45669291,  1.53543307,  1.61417323,
        1.69291339,  1.77165354,  1.8503937 ,  1.92913386,  2.00787402,
        2.08661417,  2.16535433,  2.24409449,  2.32283465,  2.4015748 ,
        2.48031496,  2.55905512,  2.63779528,  2.71653543,  2.79527559,
        2.87401575,  2.95275591,  3.03149606,  3.11023622,  3.18897638,
        3.26771654,  3.34645669,  3.42519685,  3.50393701,  3.58267717,
        3.66141732,  3.74015748,  3.81889764,  3.8976378 ,  3.97637795,
        4.05511811,  4.13385827,  4.21259843,  4.29133858,  4.37007874,
        4.4488189 ,  4.52755906,  4.60629921,  4.68503937,  4.76377953,
        4.84251969,  4.92125984,  5.        ])

Here we have implicitly accepted the default wavelength of 0.5 microns, and Q factor of 2 (Nyquist sampling) which are usually sane defaults. The pupil is circular and is sufficiently described by a Zernike expansion up to Z9.

We can plot the wavefront or PSF of the objective. The wavefront will appear to not quite fill the array, but this is just an artifact of the default lanczos interpolation and relatively few samples.

[4]:
fig, ax = pupil.plot2d(interpolation='nearest')
ax.grid(False)
ax.set_title('Wavefront')

fig, ax = psf.plot2d(xlim=20, power=1/2)  # 1/2 stretch, colorbar scales as well.
ax.grid(False)
ax.set_title('PSF');
[4]:
Text(0.5, 1.0, 'PSF')
../_images/examples_System_Model_6_1.png
../_images/examples_System_Model_6_2.png

or compute its MTF. Note that “tan” and “sag” here accept the assumption of optical design code that we are looking at an object extended in Y, with no extent in X. For example, this means we could be at an (x,y) field point of (0, 1) degrees. On-axis, tan and sag are simply misgnomers for the “x” and “y” MTFs.

[5]:
mtf = MTF.from_psf(psf)
mtf.slices().plot(['x', 'y', 'azavg'], xlim=(0,200))
[5]:
(<Figure size 432x288 with 1 Axes>,
 <AxesSubplot:xlabel='Spatial Frequency [$\\mathrm{mm^{-1}}$]', ylabel='MTF [$\\mathrm{Rel 1.0}$]'>)
../_images/examples_System_Model_8_1.png
[6]:
pixel_pitch = 5  # 5 micron diameter pixels
aa_filter = OLPF(pixel_pitch*0.66)
pixel = PixelAperture(pixel_pitch)
sys_psf = psf.conv(aa_filter).conv(pixel).renorm()  # renorm so max=1

We can plot the system PSF, which is abstract since it includes the pixel aperture. You would not normally look at this, but prysm doesn’t stop you from doing that.

[7]:
sys_psf.plot2d(xlim=20, interpolation='lanczos', power=1/2)  # sys_psf is a Convolvable, not a PSF.
plt.grid(False)
../_images/examples_System_Model_11_0.png
[8]:
sys_mtf = MTF.from_psf(sys_psf)
sys_mtf.slices().plot(['x', 'y', 'azavg'], xlim=(0,200))
[8]:
(<Figure size 432x288 with 1 Axes>,
 <AxesSubplot:xlabel='Spatial Frequency [$\\mathrm{mm^{-1}}$]', ylabel='MTF [$\\mathrm{Rel 1.0}$]'>)
../_images/examples_System_Model_12_1.png

We see the system MTF reach zero at 200 cy/mm, as would be expected for a 5 micron pixel. We also see the PSF is significantly squared off, since the pixel aperture contribution is larger than that of the optical system.

For more information on the classes used, see Zernikes, PSFs, MTFs, and PixelApertures, OLPFs, and convolutions.